// Module R5 for MPX Project
// Adam Brady and Will Sica

// BEGIN Function Prototypes
int com_open (int *eflag_p, int baud_rate);
int com_close (void);
int com_read (char *buf_p, int *count_p);
int com_write (char *buf_p, int *count_p);
void interrupt (* oldfunc) (void);
void interrupt high_level_serial(void);
void low_level_serial_input(void);
void low_level_serial_output(void);
void pic_on(void);
void pic_off(void);
// END Function Prototypes

// BEGIN Definitions
#define OPEN 		1
#define CLOSED 		2
#define READING 	4
#define WRITING 	5
#define DUAL		6
#define CLEAR_FLAG 	0
#define DONE 		1
#define ASCII_FOR_RETURN 13
#define RING_BUFFER_SIZE 100
#define INVALID_EVENT_FLAG_POINTER 	-101
#define INVALID_BAUD_RATE_DIVISOR 	-102
#define PORT_ALREADY_OPEN 			-103
#define SERIAL_PORT_NOT_OPEN 		-201
#define PORT_NOT_OPEN 				-301
#define INVALID_BUFFER_ADDRESS 		-302
#define INVALID_COUNT_ADDRESS 		-303
#define DEVICE_BUSY 				-304
#define WR_PORT_NOT_OPEN 			-401
#define WR_INVALID_BUFFER_ADDRESS 	-402
#define WR_INVALID_COUNT_ADDRESS 	-403
#define WR_DEVICE_BUSY 				-404
#define COM1_ID 			0x3F8
#define COM1_INT_ID 		0x3FA
#define COM1_INTERRUPT_ID 	0x0C
#define PIC_COMMAND 		0x20
#define PIC_MASK 			0x21
#define COM1_PIC_LEVEL 		0x10
#define END_OF_INTERRUPT 	0x20
#define IDLE 3 //Idle is defined elsewhere
#define NULL 0
// END Definitions

// BEGIN Data Structures
// Device Control Block
struct device_control_block {
	int flag_open;         // OPEN or CLOSED
	int *event_flag;       // Pointer to event flag associated with device
	int status_code;       // IDLE, READING, or WRITING
	char* in_buff;         // Pointer to the requestor's buffer (where read data will be placed)
	int *in_count;         // Counter of the maximum number of chars that can be placed in the buffer
	int in_done;           // Counter for how many characters have been placed in the requestor's buffer
	char* out_buff;        // As in_buff for output
	int *out_count;        // As in_count for output
	int out_done;          // As in_done for output
	char ring_buffer[RING_BUFFER_SIZE]; // Ring Buffer
	int ring_buffer_in;    // Index where you write the next character to the ring buffer
	int ring_buffer_out;   // Index where you will remove the next character from the buffer
	int ring_buffer_count; // Count of how many character stored but not read from the buffer
} serial_device;
// END Data Structures

// BEGIN Variables
unsigned char interrupt_code;
unsigned char input_char;
unsigned char output_char;
// END Variables

// BEGIN Interrupt Handlers
void interrupt high_level_serial()
{
	if (serial_device.flag_open != OPEN)
	{
		outportb(PIC_COMMAND, END_OF_INTERRUPT);
		return;
	}
	interrupt_code = inportb(COM1_INT_ID);
	interrupt_code = interrupt_code & 0x07;
	if (interrupt_code == 2)
	{
		low_level_serial_output(); // call write handler
	}
	else if (interrupt_code == 4)
	{
		low_level_serial_input(); // call read handler
	}
	outportb(PIC_COMMAND, END_OF_INTERRUPT); // Clear the interrupt
}
void low_level_serial_input()
{
	// Read a character from the input register
	input_char = inportb(COM1_ID);
	outportb(COM1_ID, input_char); //ECHO Function for extra credit
	if (serial_device.status_code != READING)
	{
	    if (serial_device.ring_buffer_count >= RING_BUFFER_SIZE ) 
		{ 
			return; 
		}
		else
		{
			serial_device.ring_buffer[serial_device.ring_buffer_in] = input_char;
			serial_device.ring_buffer_in += 1;
			if (serial_device.ring_buffer_in >= RING_BUFFER_SIZE)
			{
				serial_device.ring_buffer_in = 0;
			}
			serial_device.ring_buffer_count += 1;
			// store the character in the ring buffer, increment ring buffer indices
		}
	}
	else
	{
		//store in requestors input buffer
		if (input_char != ASCII_FOR_RETURN )
		{
			serial_device.in_buff[serial_device.in_done] = input_char;
			serial_device.in_done += 1;
		}
		if (input_char == ASCII_FOR_RETURN  || serial_device.in_done >= *serial_device.in_count)
		{
			if (serial_device.status_code == READING)
			{
				serial_device.status_code = IDLE; // reset status to idle
				*serial_device.event_flag = DONE; // set event flag to indicate completion of operation
			}
			else if (serial_device.status_code == DUAL)
			{
					serial_device.status_code = WRITING; 
			}
			serial_device.in_buff[serial_device.in_done] = '\0'; //null terminate the buffer
			*serial_device.in_count = serial_device.in_done + 1; // set count to done, so requestor knows how many characters were placed
		}
	}
}
void low_level_serial_output()
{
	int mask;
	if (serial_device.status_code != WRITING && serial_device.status_code != DUAL)
		return;
	if (serial_device.out_done < *serial_device.out_count)
	{
		outportb(COM1_ID, *serial_device.out_buff);
		serial_device.out_buff += 1;
		serial_device.out_done += 1;
	}
	else
	{
		if (serial_device.status_code == WRITING)
		{
			serial_device.status_code = IDLE; // reset status to idle
			*serial_device.event_flag = DONE; // set event flag to indicate completion of operation
		}
		else if (serial_device.status_code == DUAL)
		{
					serial_device.status_code = READING; 
		}
		// Disable write interrupts
		mask = inportb(COM1_ID + 1);
		mask = mask & ~0x02;
		outportb(COM1_ID + 1, mask);
	}
}
// END Interupt Handlers

// BEGIN Code
int com_open(int *eflag_p, int baud_rate)
{
	//eflag_p pointer to an integer event flag within calling program
	//baud_rate integer value representing desired baud rate
	int baud_rate_divisor;
	int mask;
	
	// Check Parameters
	if (eflag_p == NULL)
	{
		return INVALID_EVENT_FLAG_POINTER;
	}
	if (baud_rate <= 0)
	{
		return INVALID_BAUD_RATE_DIVISOR;
	}
	if (serial_device.flag_open == OPEN)
	{
		return PORT_ALREADY_OPEN;
	}
	// Initialize DCB
	serial_device.flag_open = OPEN;      // Set the device to OPEN
	serial_device.event_flag = eflag_p;  // Save event flag so can be edited
	serial_device.status_code = IDLE;    // Set device status to IDLE
	serial_device.ring_buffer_in = 0;    // Set to 0
	serial_device.ring_buffer_out = 0;   // Set to 0
	serial_device.ring_buffer_count = 0; // Set to 0
	disable(); // Disable interupts while modifying registers
	// Set interupt vector to new handler address
	oldfunc = getvect(COM1_INTERRUPT_ID); // Save the old interrupt handler
	setvect(COM1_INTERRUPT_ID, &high_level_serial); // Install the new handler
	// Compute and store the Baud Rate Divisor
	outportb(COM1_ID + 3,0x80); // Send to Line Control Register code to set BRD Register
	baud_rate_divisor = 115200 / (long) baud_rate;
	outportb(COM1_ID, baud_rate_divisor&0xFF); //Least Significant Bits of BRD register
	outportb(COM1_ID + 1, (baud_rate_divisor >> 8)&0xFF); //Most Significant Bits of BRD register
	outportb(COM1_ID + 3,0x03); // Send to Line Control Register code to return to normal operation
	// Enable overall serial port interrupts by storing the value 0x08 in the Modem Control Register
	outportb(COM1_ID + 4, 0x08);
	// Enable input ready interrupts only by storing the value 0x01 in the Interrupt Enable register
	outportb(COM1_ID + 1, 0x01);
	// Enable all of the necessary interrupts
	pic_on();
	enable(); //reenable interrupts
	return 0; //return value for no errors
}
int com_close(void)
{
	int mask;
	if (serial_device.flag_open != OPEN)
	{
		return SERIAL_PORT_NOT_OPEN;
	}
	serial_device.flag_open = CLOSED; // Close the DCB
	disable(); //touching register values
	// Load 0x00 into the Modem Status and Interrupt Enable registers to disable serial interrupts
	outportb(COM1_ID + 4,0x00);
	outportb(COM1_ID + 1,0x00);
	// Restore the original interrupt vector
	setvect(COM1_INTERRUPT_ID, oldfunc);
	pic_off();
	enable(); // reenable interrupts
	return 0; //Return value for no-errors
}
int com_read(char* buf_p, int *count_p)
{
	//buf_p: far pointer to starting address of the buffer to recieve the in characters
	//count_p: address of an integer count value indicating the number of characters to be read
	if (serial_device.flag_open != OPEN)
	{
		return PORT_NOT_OPEN;
	}
	if (buf_p == NULL)
	{
		return INVALID_BUFFER_ADDRESS;
	}
	if (count_p == NULL || &count_p <= 0)
	{
		return INVALID_COUNT_ADDRESS;
	}
	serial_device.in_buff = buf_p; // Save the address of the requesters buffer into the DCB
	serial_device.in_count = count_p; // Save the requesters count to DCBs count
	serial_device.in_done = 0; // Set the input done count to 0
	*serial_device.event_flag = NULL; // Clear the event flag to indicate operation is beginning
	if (serial_device.status_code != WRITING)
	{
		serial_device.status_code = READING; //set devices status to reading
	}
	else
	{
		serial_device.status_code = DUAL;
	}
	disable(); //disable interrupts
	// Read from Ring Buffer in to Input Buffer
	if (serial_device.ring_buffer_count > 0)
	{
		while(serial_device.in_done < *serial_device.in_count && serial_device.ring_buffer_count > 0 && serial_device.ring_buffer[serial_device.ring_buffer_out] != ASCII_FOR_RETURN )
		{
			serial_device.in_buff[serial_device.in_done] = serial_device.ring_buffer[serial_device.ring_buffer_out];
			serial_device.in_done += 1;
			serial_device.ring_buffer_out += 1;
			if (serial_device.ring_buffer_out >= RING_BUFFER_SIZE)
				serial_device.ring_buffer_out = 0;
			serial_device.ring_buffer_count -= 1;
		}
	}
	enable(); //reenable interrupts
	if(serial_device.in_done < *serial_device.in_count && serial_device.ring_buffer[serial_device.ring_buffer_out] != ASCII_FOR_RETURN ) // Check if buffer has been filled
	{
		return 0;
	}
	serial_device.in_buff[serial_device.in_done] = '\0'; //Null terminate the inputbuffer
	serial_device.status_code = IDLE;//Set status to idle
	*serial_device.event_flag = DONE;//Set event flag to indicate event completion
	return serial_device.in_done; //Return value of number of characters placed in the buffer
}
int com_write(char* buf_p, int *count_p)
{
	int mask;
	if (serial_device.flag_open != OPEN)
	{
		return WR_PORT_NOT_OPEN;
	}
	if (buf_p == NULL)
	{
		return WR_INVALID_BUFFER_ADDRESS;
	}
	if (count_p == NULL || &count_p <= 0)
	{
		return WR_INVALID_COUNT_ADDRESS;
	}
		if (serial_device.status_code != READING)
	{
		serial_device.status_code = WRITING; 
	}
	else
	{
		serial_device.status_code = DUAL;
	}
	serial_device.out_buff = buf_p; // Save the address of the requestor's buffer in the DCB's output buffer pointer
	serial_device.out_count = count_p; // Also store the count
	serial_device.out_done = 0; // Set output done to 0
	*serial_device.event_flag = NULL; // Clear the event flag to indicate the operation is beginning
	outportb(COM1_ID, *serial_device.out_buff);
	serial_device.out_buff += 1;
	serial_device.out_done += 1;
	// Enable write interrupts
	mask = inportb(COM1_ID + 1);
	mask = mask | 0x02;
	outportb(COM1_ID + 1, mask);
	return 0;
}
void pic_on()
{
	int mask;
	disable();
	// Enable the appropriate level in the PIC mask register
	mask = inportb(PIC_MASK);
	mask = mask & ~COM1_PIC_LEVEL;
	outportb(PIC_MASK, mask);
	enable();
}
void pic_off()
{
	int mask;
	disable();
	//Disable the appropriate level in the PIC mask register
	mask = inportb(PIC_MASK);
	mask = mask | COM1_PIC_LEVEL;
	outportb(PIC_MASK, mask);
	enable();
}
// END Code